js 代码片段实现,只聚焦当前文档所在的目录树
需求
在使用“始终定位打开的文档”功能时,只展开当前文档的文档树,自动关闭(不展开)其它无关的文档树。
有笔记本 A,内有文档 A1,子文档 A2、子子文档 A3。
有笔记本 B,内有文档 B1,子文档 B2、子子文档 B3。
点击 A2 页签,左侧文档树自动展开,并定位到 A-A1-A2;A3 列表不展开
点击 B2 页签,左侧文档树自动展开,定位到 B-B1-B2;同时关闭 A-A1-A2 文档树的展开状态,恢复到笔记本 A。也就是仅保留当前活动页签对应的文档树是展开状态,其它文档树分支全部关闭,包括当前活动文档的子文档树。
实现
js
// 加载时是否自动定位当前文档
const autoFocusTreeOnload = true;
// 等待标签页容器渲染完成后开始监听
whenElementExist('.layout__center').then(async element => {
// 等待笔记列表加载完毕
await sleep(40);
// 监听页签切换事件
observeTabChanged(element, (tab) => {
// 折叠所有笔记,然后定位当前笔记
collapseAllBooksThenFocusCurrentBook(element, tab);
});
// 加载时定位当前文档
if(autoFocusTreeOnload) {
await sleep(40);
focusCurrentDocInTrees();
}
});
// 折叠所有笔记,然后定位当前笔记
async function collapseAllBooksThenFocusCurrentBook(element, tab) {
let currNodeId = "";
// 等待激活文档加载完毕
await whenElementExist(()=>{
const content = element.querySelector('.layout-tab-container [data-id="'+tab.getAttribute("data-id")+'"]');
// 获取当前文档的node-id
currNodeId = document.querySelector('.layout-tab-container [data-id="'+tab.getAttribute("data-id")+'"] .protyle-title')?.getAttribute("data-node-id") || "";
return content && content.getAttribute("data-loading") === "finished" && currNodeId;
});
// 获取当前文档所在的路径和当前笔记,并折叠当前文档以外的目录
let currBoxDataUrl = "";
if(currNodeId){
// 定位当前文档
focusCurrentDocInTrees();
// 等待定位完成
let currTreeItem = null;
await whenElementExist(()=>{
currTreeItem = document.querySelector("ul.b3-list[data-url] li[data-node-id='" + currNodeId + "']");
return currTreeItem;
});
// 获取当前文档在目录树中的node-id
if(currTreeItem) {
// 获取当前笔记的data-url
currBoxDataUrl = currTreeItem.closest('ul.b3-list[data-url]')?.getAttribute("data-url") || "";
if(currBoxDataUrl){
// 获取当前文档所在的路径
currTreeItemPath = currTreeItem.getAttribute("data-path");
// 获取所有展开的目录
const trees = document.querySelectorAll("ul.b3-list[data-url='" + currBoxDataUrl + "'] span.b3-list-item__toggle svg.b3-list-item__arrow--open");
trees.forEach(arrowBtn => {
// 如果是当前文档的上级目录则跳过
const itemPath = arrowBtn.closest('li.b3-list-item')?.getAttribute("data-path")?.replace(/\.sy$/i, '');
if(currTreeItemPath.startsWith(itemPath)){
return;
}
// 其他目录则折叠
if (arrowBtn.parentElement) {
arrowBtn.parentElement.click();
}
});
}
}
}
// 折叠除当前笔记外的所有笔记
document.querySelectorAll("ul.b3-list[data-url]").forEach(async book => {
// 如果是当前笔记则跳过
if(book.getAttribute("data-url") === currBoxDataUrl) {
return;
}
// 折叠笔记
const bookArrowBtn = book.querySelector('li[data-type="navigation-root"] span.b3-list-item__toggle');
if (bookArrowBtn && bookArrowBtn.firstElementChild.classList.contains("b3-list-item__arrow--open")) {
bookArrowBtn.click();
}
});
}
// 在目录树中定位当前文档
async function focusCurrentDocInTrees() {
// 定位当前文档
document.querySelector(".layout-tab-container .block__icons span[data-type=focus]")?.click();
// 等待定位完成
await whenElementExist(()=>{
return document.querySelector('ul.b3-list[data-url] li[data-type="navigation-file"].b3-list-item--focus');
});
// 处理官方定位,在未打开目录树时,左侧dock区目录树显示隐藏按钮样式会获取焦点的bug
await sleep(40);
const dockFileTreeBtn = document.querySelector('#dockLeft span[data-type="file"]');
if(document.querySelector('#layouts .layout__dockl')?.style?.width === "0px"){
if(dockFileTreeBtn.classList.contains("dock__item--active")) {
dockFileTreeBtn.classList.remove("dock__item--active");
}
if(dockFileTreeBtn.classList.contains("dock__item--activefocus")) {
dockFileTreeBtn.classList.remove("dock__item--activefocus");
}
}
}
// 监听页签切换事件
function observeTabChanged(parentNode, callback) {
// 创建一个回调函数来处理观察到的变化
const observerCallback = function(mutationsList, observer) {
// 用常规方式遍历 mutationsList 中的每一个 mutation
for (let mutation of mutationsList) {
// 属性被修改
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
const element = mutation.target;
if (element.tagName.toLowerCase() === 'li' && element.getAttribute('data-type') === 'tab-header' && element.classList.contains('item--focus')) {
if(typeof callback === 'function') callback(element);
}
}
// 如果有新的子节点被添加
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE && node.tagName.toLowerCase() === 'li') {
if (node.getAttribute('data-type') === 'tab-header' && node.classList.contains('item--focus')) {
if(typeof callback === 'function') callback(node);
}
}
});
}
}
};
// 创建一个观察器实例并传入回调函数
const observer = new MutationObserver(observerCallback);
// 配置观察器:传递一个对象来指定观察器的行为
const config = { attributes: true, attributeFilter: ['class'], childList: true, subtree: true };
// 开始观察目标节点
observer.observe(parentNode, config);
// 返回一个函数,用于停止观察
return function stopObserving() {
observer.disconnect();
};
}
// 延迟执行
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 等待元素渲染完成后执行
function whenElementExist(selector) {
return new Promise(resolve => {
const checkForElement = () => {
let element = null;
if (typeof selector === 'function') {
element = selector();
} else {
element = document.querySelector(selector);
}
if (element) {
resolve(element);
} else {
requestAnimationFrame(checkForElement);
}
};
checkForElement();
});
}
效果
使用方法
设置 》外观 》代码片段 》js 中新增加代码片段,然后把上面的代码粘贴过去即可。
最后
由于刚接触思源,对思源 api 还不是很了解,所以选择用 js 代码片段实现,纯原生 js 实现,方法比较笨,勉强能用。